- 49 - Г Л А В А 3 (Андрэ Ла Мот) ----------------- ОСНОВЫ РАБОТЫ С УСТРОЙСТВАМИ ВВОДА В настоящее время компьютеры еще ненаучились читать наши мысли, а раз так, то для того, чтобы хоть как-то общаться с ними и "объяснять" им, что же мы от них хотим, нам приходится "разговаривать" с ними через механические интерфейсы. В понятие интерфейс входят клавиатура, джойстик и мышь. Для работы с этими устройствами мы будем придерживаться стандартной тактики - использовать BIOS, прерывания и ассемблер. Эта глава состоит из следующих частей: - Взаимодействие с пользователем в видеоиграх; _ Джойстик и работа с ним; - Работа с клавиатурой; - Управление мышью. ВЗАИМОДЕЙСТВИЕ С ПОЛЬЗОВАТЕЛЕМ В ВИДЕОИГРАХ Компьютерные игры не были бы так азартны, если бы мы не могли влиять на разворачивающиеся в них события. Основная идея видеоигры состоит в возможности взаимодействия - 50 - с "виртуальным миром" в реальном масштабе времени. Когда видеоигры только появились, они имели совершенно убогий интерфейс - многие из них управлялись только парой клавиш. Но время шло и появились новые устройства ввода. Фирма Atari и некоторые другие начали выпускать игры, управляемые джойстиками и более хитрыми устройствами типа "пилотского штурвала". Сегодня для ПК создано множество устройств - плод кропотливых теоретических и практических инженерных разработок. Как создатели видеоигр, мы должны уделять пристальное внимание работе с устройствами ввода. Люди, играющие в наши игры, должны иметь удобный и надежный интерфейс для изучения созданного нами мира. Если же взаимодействие с системой неудобно и сложно, не поможет ни красивая музыка, ни видеоэффекты - людей не привлечет игра, которой они не смогут управлять. Надо сказать, что сейчас очень удачное время для тех, кто собирается заняться видеоигорным бизнесом. Сегодня мы можем выделить среди пользователей ПК несколько болбших групп Например,: - Большинство имеет компьютеры 386 и 486; - У большинства есть, как минимум, VGA-карты и многие располагают звуковыми картами; - Наконец, благодаря президенту Microsoft Билу Гейтсу и большой популярности оболочки Windows, практически у всех есть мыши. Можно задать вопрос: "А имеет ли предполагаемый игрок джойстик?" Ответ будет менее определенным: "Кто-то имеет, а кто-то - нет". Многие все же предпочитают клавиатуру. Неважно, какое из устройств используется, мы должны добиться, чтобы его применение было для игрока почти интуитивным. Не в нашей власти выбирать, какое из устройств должно поддерживаться игрой. Мы обязаны научиться работать с любым из них. В данном случае мы должны стать рабами тех, для кого пишем игры, хотя наша философия и привычки (в выборе устройств ввода) могут сильно отличаться от привычек наших игроков. Теперь без лишних слов начнем с джойстика. ДЖОЙСТИК Одному богу известно, за что джойстик получил столь неуклюжее имя*. Интерфейс джойстика с ПК тоже нельзя назвать продуманным, да и аппаратная часть весьма неудобна (правда, здесь не все могут согласиться со мной). Таким образом, по сравнению с другими компьютерами работа с джойстиком на ПК весьма не ортодоксальна и противоречива, но и не так сложна, как может показаться на первый взгляд. Как видно из рис.3.1, джойстик - это ____________________ * Joystick дословно означает "рукоятка, доставляющая радость" (прим.перев.) - 50 - Рис.3.1. Динамика механических движений джойстика. аналоговое устройство, которое изменяет значение сигнала на выходе в зависимости от положения рукоятки. Наша задача состоит в преобразовании этого аналогового сигнала (его величины) в более приемлемый вид, а именно , в цифровое значение. Все мы, конечно, знаем про АЦП, и если бы ПК создавали сегодня, то он непременно стоял бы в каждой карте порта джойстика. Но в конце 70-х, начале 80-хгодов, когда ПК только разрабатывались, все АЦП были очень дороги. Тогда инженеры создали специальный АЦП только для контроллера джойстика. Для того времени это было гениальным решением, но сегодня заставляет ломать голову над программированием джойстика каждого программиста. Как работает джойстик --------------------- Теперь поговорим более подробно о том, как работает джойстик: - Каждая ось джойстика имеет связанный с ней потенциометр. Когда рукоятка отклоняется по оси Х или Y, то сопротивление соответствующего потенчиометра изменяется; - Потенциометр используется вместе с конденсатором для создания цепи нагрузки; - Как мы знаем из курса элементарной электроники, если подать напряжение на цепь, состоящую из последовательно включенного сопротивления и - 52 - конденсатора, то время зарядки конденсатора будет пропорционально величине сопротивления и напряжения; - Напряжение снимается с конденсатора и сравнивается с эталонным. Когда напряжение достигает порогового значения, система выставляет флаг; - Время, занимаемое этим процессом, пропорционально сопротивлению, которое в свою очередь зависит от позиции ручки джойстика. Таким образом, мы можем определить положение джойстика. К сожалению, во всем этом есть одна проблема: мы измеряем время реакции системы, но в данном случае измерение означает счет. И чем быстрее ПК, тем быстрее он считает. Таким образом, более быстродействующие ПК будут отличаться от медленных по получаемым результатам. Именно поэтому прежде чем начать играть в игру, нам приходится поиграть в калибровку джойстика. Программа должна запомнить параметры джойстика для данного ПК. Кнопки джойстика ------------------ Кнопки джойстика - это обычные переключатели (кстати, вам крупно повезло, ведь это легко моглибыть зарядные, ионообменные управляемые магнитным полем подушечки). Для того чтобы узнать состояние переключателя, нам надо прочитать значение из определенного порта. I/O порт 0х201h-игровой порт ┌────┬───┬───┬───┬───┬───┬───┬───┬ │ X │ X │ X │ X │ X │ X │ X │ X │ └──┬─┴─┬─┴─┬─┴─┬─┴──┬┴─┬─┴─┬─┴─┬─┴ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─── 0 бит: Джойстик А ось х │ │ │ │ │ │ │ │ │ │ │ │ │ └─────── 1 бит: Джойстик А ось у │ │ │ │ │ │ │ │ │ │ │ └────────────2 бит: Джойстик В ось х │ │ │ │ └───────────────3 бит: Джойстик В ось у │ │ │ └────────────────────4 бит: Джойстик А кнопка 1 │ │ └────────────────────────5 бит: Джойстик А кнопка 2 │ └────────────────────────────6 бит: Джойстик В кнопка 1 └────────────────────────────────7 бит: Джойстик В кнопка 2 Рис.3.2. Раскладка битовых значений для порта джойстика. - 53 - Есть джойстики с множеством кнопок, например, Trustmaster. Несмотря ни на что, его работа ничем не отличается от других джойстиков, а состояние лишних кнопок можно прочитать через другие порты ввода/вывода. Эти порты обычно указываются производителем в соответствующей документации. Обычно, порт 201h - это то окно, через которое можно общаться с джойстиком. Как показано на рис.3.2, этот порт связан и с джойстиком А, и сджойстиком В. Мы узнаем назначение битов 0-3 чуть позже - в разделе "Чтение позиции джойстика". Биты 4-7 предназначены для чтения кнопок джойстика. Когда одна из кнопок нажата, то соответствующий бит порта 201h изменяется. Есть только одна маленькая деталь: эначения битов всегда инвертированы. Это значит, что если вы , например, жмете кнопку 1 на джойстике А, то бит 0 изменит значение с 1 на 0. Но в общем, это не особенно принципиально. Листинг 3.1. Чтение кнопок джойстика. _________________________________________________________________________ #define JOYPORT // порт джойстика = 201h #define BUTTON_1_A 0х10 // джойстик А, кнопка 1 #define BUTTON_1_B 0х20 // джойстик А, кнопка 2 #define BUTTON_2_A 0х40 // джойстик В, кнопка 1 #define BUTTON_2_B 0х80 // джойстик В, кнопка 2 #define JOYSTICK_1_X 0x01 // джойстик А, ось Х #define JOYSTICK_1_Y 0x02 // джойстик А, ось У #define JOYSTICK_2_X 0x04 // джойстик В, ось Х #define JOYSTICK_2_Y 0x08 // джойстик В, ось У #define JOY_1_CAL 1 // эта команда калибрует джойстик А #define JOY_2_CAL 2 // эта команда калибрует джойстик В unsigned char Buttons(unsigned char button) { // эта функция читает статус кнопок джойстика // сбрасываем содержимое порта 201h outp(JOYPORT,0); // инвертируем прочитанное из порта значение и комбинируем // его с маской return(~inp(JOYPORT) & button); } - 54 - unsigned char Buttons_Bios(unsigned char button) { // чтение кнопок через обращение к BIOS union _REGS inregs, outregs; inregs.h.ah = 0x84; // функция джойстика 84h inregs.x.dx = 0x00; // подфункция 0h - чтение кнопок // вызов BIOS _int86(0x15,&inregs,&outregs); // инвертируем полученное значение и комбинируем его с маской return(~outregs.h.al) & button); } _________________________________________________________________________ Теперь посмотрим на детали Листинга 3.1. - Функция Buttons() и Buttons_Bios() возвращают одинаковый результат. Buttons() посылает 0 в порт джойстика (это делается для того, чтобы инициировать порт) и затем читает данные; - Как только данные получены, мы маскируем младшие 4 бита и инвертируем 4 старших; - Этот листинг включает также определение констант (#define), что делает интерфейс более удобным; - Buttons_Bios() для чтения джойстика использует BIOS. Как только выполняется вызов, результат помещается в регистр AL. В принципе, для таких простых вещей, как кнопки, я использую прямой доступ к портам. Я уже говорил, что использование функций BIOS более медлительно. Правда, по отношению к джойстику это, может быть, и не самый плохой подход. Если вы хотите читать с помощью BIOS - читайте. Чтение позиции джойстика ------------------------- Чтение позиции джойстика - весьма утомительная, но вполне выполнимая задача. Все, что нам надо сделать, это послать джойстику простую команду. Это делается записью значения 0 в порт 201h. Затем мы ждем, когда установится нужный нам бит (0-3) порта джойстика. Вовремя ожидания мы должны включить счетчик. Когда нужный бит установлен, то число, которое мы насчитаем, и есть позиция джойстика. Листинг 3.2 показывает код, который все это делает. - 55 - Листинг 3.2. Чтение позиции джойстика. _________________________________________________________________________ unsigned int Joystick(unsigned char stick) { _asm { cli ; запретить прерывания mov ah,byte ptr stick ; замаскировать АН, ; чтобы выбрать джойстик xor al,al ; обнулить AL xor cx,cx ; обнулить CX mov dx,JOYPORT ; используем DX для ввода и вывода out dx,al discharge: in al,dx ; читаем данные из порта test al,ah ; изменился ли бит запроса? loopne discharge ; если нет, повторяем чтение sti ; разрешить прерывания xor ax, ax ; обнулить AX sub ax,cx ; теперь AX содержит позицию джойстика } // конец ассемблерного блока // возвращаемое значение содержится в AX } // конец функции __________________________________________________________________________ (Кстати, встроенный ассемблер мне все больше и больше нравится.) Программа достаточно проста: - При запуске программа обнуляет регистры АХ и СХ; - Затем программа опрашивает порт джойстика; - Далее подсчитывается количесто циклов в ожидании, пока установится нужный бит; - Подсчет выполняется в регистре СХ с помощью инструкции LOOPXX (в данном случае используется команда LOOPNE); - Инструкция TEST определяет установку бита; - Когда нужный бит установлен, программа выходит из цикла. Результат передается вызывающей программе, в регистре АХ. С этим вроде все. Позже я покажу демонстрационную программу, в которой применяются все функции джойстика. Благодаря обращению к BIOS она обладает лучшей совместимостью, легче переносима и умеет производить самокалибровку. Я думаю, что и вы будете пользоваться ею. - 56 - Калибровка джойстика ------------------------- Теперь разберемся с калибровкой джойстика. Как я уже говорил ранее, результат, который мы получим от чтения джойстика в цикле, будет разным на разных ПК. На одних компьютерах значения по оси Х окажется в пределах от 0 до 255, на других - от 0 до 10000. Таким образом, нам надо нормализовать эти данные или масштабировать их. Стандартным приемом в данных случаях может служить калибровка джойстика самим игроком в setup`е игры. Во время этой процедуры игрок двигает джойстиком, а программа считывает и запоминает данные калибровки где-нибудь на диске для дальнейшего использования. Для того, чтобы проанализировать джойстик, программа должна: - Найти значения максимального и минимального отклонения по осям X и Y; - Сохранить эту информацию; - Использовать полученные данные для выяснения, на какой угол игрок отклонил ручку джойстика. Например, джойстик был откалиброван и мы обнаружим, что ось Х имеет значения от 0 до 255. Затем, если значение джойстика, например, по координате Х окажется равным 128, то можно с уверенностью сказать, что рукоятка находится в среднем положении (кстати, в процессе калибровки средняя позиция также запоминается). Осталась одна маленькая деталь, о которой стоит сказать - это детектирование джойстика (то есть проверка его наличия). Обычная техника детектирования такая: в цикле опрашивается порт джойстика в течение определенного времени. Но надо сказать, что с помощью функций BIOS это можно сделать более надежно. Если после вызова функции значения по всем координатам равны 0, то никакого джойстика нет. Я думаю, что мы уже сказали о джойстиках все. Теперь напишем небольшую программку, в которой используем функции из Листингов 3.1 и 3.2. При старте она просит игрока: - Подвигать джойстиком; - Установить джойстик в среднее положение; - Понажимать на кнопки. Затем, программа сохраняет результаты процедуры калибровки в глобальных переменных. - 57 - Листинг 3.3. Программа работы с джойстиком (JOY.C). _________________________________________________________________________ // I N C L U D E S ////////////////////////////////////////////////////////// #include #include #include #include #include #include // D E F I N E S /////////////////////////////////////////////////////////// #define JOYPORT 0x201 // joyport is at 201 hex #define BUTTON_1_A 0x10 // joystick 1, button A #define BUTTON_1_B 0x20 // joystick 1, button B #define BUTTON_2_A 0x40 // joystick 2, button A #define BUTTON_2_B 0x80 // joystick 2, button B #define JOYSTICK_1_X 0x01 // joystick 1, x axis #define JOYSTICK_1_Y 0x02 // joystick 1, y axis #define JOYSTICK_2_X 0x04 // joystick 2, x axis #define JOYSTICK_2_Y 0x08 // joystick 2, y axis #define JOY_1_CAL 1 // command to calibrate joystick #1 #define JOY_2_CAL 2 // command to calibrate joystick #2 // G L O B A L S /////////////////////////////////////////////////////////// unsigned int joy_1_max_x, // global joystick calibration variables joy_1_max_y, joy_1_min_x, joy_1_min_y, joy_1_cx, joy_1_cy, joy_2_max_x, joy_2_max_y, joy_2_min_x, joy_2_min_y, joy_2_cx, joy_2_cy; // F U N C T I O N S //////////////////////////////////////////////////////// unsigned char Buttons(unsigned char button) { // read the joystick buttons by peeking the port that the switches are // attached to. outp(JOYPORT,0); // clear the latch and request a sample // invert buttons then mask with request return( ~inp(JOYPORT) & button); } // end Buttons ///////////////////////////////////////////////////////////////////////////// unsigned int Joystick(unsigned char stick) { // reads the joystick values manually by conting how long the capacitors take // to charge/discharge // let's use the inline assembler. It's Cool! __asm { cli ; disable interupts mov ah, byte ptr stick ; get mask into ah to selct joystick to read xor al,al ; zero out al, xor is a trick xor cx,cx ; same with cx which we will use as a counter mov dx,JOYPORT ; dx is used by inp and outp out dx,al ; write 0's to the port discharge: in al,dx ; read the data back from port test al,ah ; has the bit in question changed? loopne discharge ; if the stick isn't ready then --cx and loop sti ; re-enable interrupts xor ax,ax ; zero out ax sub ax,cx ; ax now holds the position of the axis switch } // end asm // since ax has the result the function will return it properly } // end Joystick ///////////////////////////////////////////////////////////////////////////// unsigned int Joystick_Bios(unsigned char stick) { // bios version of joystick read union _REGS inregs, outregs; inregs.h.ah = 0x84; // joystick function 84h inregs.x.dx = 0x01; // read joysticks subfunction 1h // call dos _int86(0x15,&inregs, &outregs); // return proper value depending on sent command switch(stick) { case JOYSTICK_1_X: { return(outregs.x.ax); } break; case JOYSTICK_1_Y: { return(outregs.x.bx); } break; case JOYSTICK_2_X: { return(outregs.x.cx); } break; case JOYSTICK_2_Y: { return(outregs.x.dx); } break; default:break; } // end switch stick } // end Joystick_Bios ///////////////////////////////////////////////////////////////////////////// unsigned char Buttons_Bios(unsigned char button) { // bios version of buttons read union _REGS inregs, outregs; inregs.h.ah = 0x84; // joystick function 84h inregs.x.dx = 0x00; // read buttons subfunction 0h // call dos _int86(0x15,&inregs, &outregs); // invert buttons then mask with request return( (~outregs.h.al) & button); } // end Buttons_Bios ///////////////////////////////////////////////////////////////////////////// void Joystick_Calibrate(int stick) { // calibrates the joystick by finding the min and max deflections in both the // X and Y axis. Then stores it in a global data structure for future use. unsigned int x_new,y_new; // temp joystick positions // set vars so that we can find there actual values if (stick==JOY_1_CAL) { printf("\nCalibrating Joystick #1: Swirl stick then release and press fire"); // set calibrations to impossible values joy_1_max_x=0; joy_1_max_y=0; joy_1_min_x=10000; joy_1_min_y=10000; // now the user should shwirl joystick let the stick fall neutral then // press any button while(!Buttons(BUTTON_1_A | BUTTON_1_B)) { // get the new values and try to update calibration x_new = Joystick_Bios(JOYSTICK_1_X); y_new = Joystick_Bios(JOYSTICK_1_Y); // process X - axis if (x_new >= joy_1_max_x) joy_1_max_x = x_new; if (x_new <= joy_1_min_x) joy_1_min_x = x_new; // process Y - axis if (y_new >= joy_1_max_y) joy_1_max_y = y_new; if (y_new <= joy_1_min_y) joy_1_min_y = y_new; } // end while // user has let stick go to center so that must be the center joy_1_cx = x_new; joy_1_cy = y_new; } // end calibrate joystick #1 else if (stick==JOY_2_CAL) { printf ("\nCalibrating Joystick #2: Swirl stick then release and press fire"); // set calibrations to impossible values joy_2_max_x=0; joy_2_max_y=0; joy_2_min_x=10000; joy_2_min_y=10000; // now the user should shwirl joystick let the stick fall neutral then // press any button while(!Buttons(BUTTON_2_A | BUTTON_2_B)) { // get the new values and try to update calibration x_new = Joystick(JOYSTICK_2_X); y_new = Joystick(JOYSTICK_2_Y); // process X - axis if (x_new >= joy_2_max_x) joy_2_max_x = x_new; else if (x_new <= joy_2_min_x) joy_2_min_x = x_new; // process Y - axis if (y_new >= joy_2_max_y) joy_2_max_y = y_new; else if (y_new <= joy_2_min_y) joy_2_min_y = y_new; } // end while // user has let stick go to center so that must be the center joy_2_cx = x_new; joy_2_cy = y_new; } // end calibrate joystick #2 printf("\nCalibration Complete...hit any key to continue."); getch(); } // end Joystick_Calibrate ///////////////////////////////////////////////////////////////////////////// // ОСНОВНАЯ ПРОГРАММА //////////////////////////////// void main(void) // to test the joystick interface { // calibrate the joystick Joystick_Calibrate(JOY_1_CAL); _clearscreen(_GCLEARSCREEN); // let user fiddle with the joystick while(!kbhit()) { _settextposition(2,0); printf("Joystick 1 = [%u,%u] ", Joystick_Bios(JOYSTICK_1_X),Joystick_Bios(JOYSTICK_1_Y)); if (Buttons_Bios(BUTTON_1_A)) printf("\nButton 1 pressed "); else if (Buttons_Bios(BUTTON_1_B)) printf("\nButton 2 pressed "); else printf("\nNo Button Pressed "); } // end while // let user know what the calibrations turned out to be printf("\nmax x=%u, max y=%u,min x=%u,min y=%u,cx=%u,cy=%u",joy_1_max_x, joy_1_max_y, joy_1_min_x, joy_1_min_y, joy_1_cx, joy_1_cy); // later! } // end main _________________________________________________________________________ Если вы введете программу с Листинга 3.3, то увидите, как джойстик А изменяет свои значения в процессе работы с ним. - 63 - КЛАВИАТУРА КЛАВИАТУРА - это наиболее сложное устройство ввода, которое есть в ПК. Она даже имеет свою собственную микросхему - контроллер ввода. Я провел много бессонных ночей, вчитываясь в листинги BIOS и пытаясь понять тайны, скрытые в работе с клавиатурой. В этой жизни есть множество непонятных вещей - курс доллара, термический коэффициент расширения рубидия и т.д. Несомненно одно - любовь людей к клавиатуре абсолютно необъяснима. Для наших целей (для написания видеоигр) мы должны научиться хорошо работать с клавиатурой. для этого вовсе не стоит разбираться с прерываниями, регистрами и портами. Мы будем использовать функции языка Си и BIOS для работы с очередью клавиатуры. Говоря о Си, я не имею в виду функции типа getch() и scanf(). Речь пойдет, скорее, о функциях типа _bios_keyboard(). Примечание ------------- Давайте приостановимся и немного подумаем. Общее правило для авторов игр - никогда не использовать BIOS. Верно? Хорошо, на самом деле BIOS вполне можно использовать для файловых операций и для выделения памяти. В общем, обращения к BIOS вполне допустимы в функциях, некритичных по времени. Попытка использовать его для работы с джойстиком или клавиатурой не будет для нас смертельна (в отличие от попыток организовать через BIOS вывод графики). Как я уже говорил, современные компьютеры достаточно быстры, чтобы нам не приходилось оптимизировать каждую запятую в тексте программы или писать ее целиком на ассемблере. _____________________ BIOS поддерживает несколько функций, которые мы будем использовать и которые приведены в табл.3.1. Таблица 3.1. Клавиатурные функции BIOS. _________________________________________________________________________ Bios INT 16h _________________________________________________________________________ Функция 00h - чтение символа с клавиатуры. _________________________________________________________________________ Вход: AH:00h Выход: AH - скан код AL - ASCII-символ - 64 - _________________________________________________________________________ Функция 01h - чтение статуса клавиатуры. _________________________________________________________________________ Вход: AH:01h Выход: AH - скан код AL - ASCII-символ флаг Z: если 0, то в буфере есть символ, если 1 - нет символа. __________________________________________________________________________ Функция 02h - Флаги, возвращаемые клавиатурой. __________________________________________________________________________ Вход: AH:02h Выход: AL - байт статуса клавиатуры: бит 0 - нажат правый Shift; бит 1 - нажат левый Shift; бит 2 - нажата клавиша Ctrl; бит 3 - нажата клавиша Alt; бит 4 - Scroll Lock в положении ON; бит 5 - Num Lock в положении ON; бит 6 - Caps Lock в положении ON; бит 7 - Insert в положении ON. _________________________________________________________________________ СКАН-КОДЫ Давайте теперь поговорим о такой вещи, как скан-коды. Если вы считаете, что при нажатии клавиши А обработчик клавиатуры также получает код символа А, то вы ошибаетесь. К сожалению, это не так. Обработчику посылается скан-код. Более того, он посылается дважды - при нажатии и отпускании клавиши. В видеоиграх нас будут интересовать не столько ASCII-коды, сколько нажатия клавиш A, S, Пробел, которые обычно отвечают за маневры, стрельбу и т.д. Таким образом, нам надо знать, как получить именно скан-коды. И это все, что требуется. В табл.3.2 перечислены скан-коды клавиш. Таблица 3.2. Таблица скан-кодов. _________________________________________________________________________ Клавиша Скан-код │ Клавиша Скан-код │ Клавиша Скан-код -----------------------│------------------------│----------------------- Esc 1 │ A 30 │ F1 59 1 2 │ S 31 │ F2 60 2 3 │ D 32 │ F3 61 ------------------------------------------------------------------------- - 65 - -----------------------│------------------------│------------------------ Клавиша Скан-код │ Клавиша Скан-код │ Клавиша Скан-код -----------------------│------------------------│------------------------ 3 4 │ F 33 │ F4 62 4 5 │ G 34 │ F5 63 5 6 │ H 35 │ F6 64 6 7 │ J 36 │ F7 65 7 8 │ K 37 │ F8 66 8 9 │ L 38 │ F9 67 9 10 │ ; 39 │ F10 68 0 11 │ Апостроф 40 │ F11 133 Минус(-) 12 │ ~ 41 │ F12 134 Равно(=) 13 │ Левый Shift 42 │ Num Lock 69 Back Space 14 │ \ 43 │ Scroll Lock 70 Tab 15 │ Z 44 │ Home 71 Q 16 │ X 45 │ Up 72 W 17 │ C 46 │ PgUp 73 E 18 │ V 47 │ Серый - 74 R 19 │ B 48 │ Left 75 T 20 │ N 49 │ 5 на цифр.клав. 76 Y 21 │ M 50 │ Right 77 U 22 │ Запятая 51 │ Серый + 78 I 23 │ Точка 52 │ End 79 O 24 │ / 53 │ Down 80 P 25 │ Правый Shift 54 │ PgDn 81 [ 26 │ Print Screen 55 │ Ins 82 ] 27 │ Alt 56 │ Del 83 Enter 28 │ Пробел 57 │ Ctrl 29 │ Caps Lock 58 │ -------------------------------------------------------------------------- Если вы внимательно изучали таблицу, то должны были заметить, что клавиши, имеющие двухсимвольную кодировку, обладают, тем не менее, только - 66 - одним скан-кодом. Это происходит потому, что каждый скан-код может быть дополнен информацией о статусе клавиш. Кроме того, благодаря таблице 3.2 мы теперь сами можем по скан-коду определять код ASCII. Статус клавиш ------------------ Мы должны иметь возможность определять: - Была ли нажата какая-нибудь клавиша; - Какая была нажата клавиша, - Статус клавиши Shift. Статус клавиш - это просто битовый вектор (последовательность), содержащий информацию о клавишах Shift, Alt, Ctrl и других. Эта последовательность находится в памяти по адресам 417h и 418h. Мы не будем читать эти ячейки напрямую, а воспользуемся BIOS и Си. Листинг 3.4 содержит код, позволяющий получить статус клавиш. Листинг 3.4. Получение статуса клавиш. ------------------------------------------------------------------------- #define SHIFT_R 0x0001 #define SHIFT_L 0x0002 #define CTRL 0x0004 #define ALT 0x0008 #define SCROLL_LOCK_ON 0x0010 #define NUM_LOCK_ON 0x0020 #define CAPS_LOCK_ON 0x0040 #define INSERT_MODE 0x0080 #define CTRL_L 0x0100 #define ALT_L 0x0200 #define CTRL_R 0x0400 #define ALT_R 0x0800 #define SCROLL_LOCK_DWN 0x1000 #define NUM_LOCK_DWN 0x2000 #define CAPS_LOCK_DWN 0x4000 #define SYS_REQ_DWN 0x8000 unsigned int Get_Control_Keys(unsigned int mask) { // функция возвращает статус интересующей нас управляющей клавиши return(mask & _bios_keybrd(_KEYBRD_SHIFTSTATUS)); } // конец функции -------------------------------------------------------------------------- В листинге 3.4 функция Get_Control_Key() использует вызов BIOS из Си для определения статуса клавиш. В строки #define включены описания масок - 67 - для определения статусных клавиш, благодаря чему вы можете вызывать функцию Get_Control_Key(), не задумываясь о значении битов состояния. Более того, используя маски и логический оператор AND, за один вызов можно получить сразу несколько состояний. Получение скан-кодов с клавиатуры ----------------------------------- Код, представленный в Листинге 3.5, напрямую считывает скан-код и возвращает его в вызывающую программу. Если ввода нет, то функция возвращает 0. Листинг 3.5. Получение скан-кодов с клавиатуры. ------------------------------------------------------------------------- unsigned char Get_Scan_Code(void) { // получить скан-код нажатой клавиши // используется встроенный ассемблер // клавиша нажата ? _asm { mov ah,01h ; функция 01h - проверка на нажатие клавиш int 16h ; вызвать прерывание jz empty ; нет нажатых клавиш - выходим mov ah,00h ; функция 0 - получить скан-код int 16h ; вызвать прерывание mov al,ah ; результат поместить в AL xor ah,ah ; обнуляем АН jmp done ; в AX возвращается значение "все в порядке" empty: xor ax,ax ; очистить AX done: } // конец ассемблерного блока } // конец функции -------------------------------------------------------------------------- Мы опять используем встроенный ассемблер. Можно было, конечно, использовать функцию BIOS через вызов _int86() в Си, но на встроенном ассемблере это выглядит намного круче. Получение ASSCII-кодов с клавиатуры ---------------------------------------- Давайте теперь посмотрим, как мы можем получить ASCII-символ, введенный с клавиатуры. Это может быть полезно, когда игрок вводит свое имя и нам нужны ASCII-коды. Мы можем получить скан-коды и транслировать их в ASCII, но к чему такие сложности, если сразу можно прочитать ASCII-коды ? - 68 - Листинг 3.6 показывает функцию, которую мы будем часто использовать, работая с клавиатурой. Эта программа опрашивает клавиши и определяет их нажатие. Если символ введен, то функция возвращает его ASCII-код, в противном случае возвращается 0. Листинг 3.6. Получение ASCII-кодов с клавиатуры. ------------------------------------------------------------------------- unsigned char Get_Ascii_Key(void) { // если это нормальный ascii код - возвращаем его, иначе 0 if (_bios_keybrd(_KEYBRD_READY)) return(_bios_keybrd(_KEYBRD_READ)); else return(0); } // конец функции __________________________________________________________________________ Чтобы использовать функцию из Листинга 3.6, вы должны выполнить примерно следующие действия: if (( c=Get_Ascii_Key()) > 0) { обработать_символ } иначе { символов_нет } Теперь все вместе: Демонстрационная программа работы с клавиатурой --------------------------------------------------------------------- Как теперь насчет того, чтобы собрать все написанное про клавиатуру в одну кучу? В Листинге 3.7 представлена демонстрационная программа, которая состоит из вызовов уже написанных в этой главе функций. Она показывает скан-коды нажатых клавиш и состояние клавиш Ctrl и Alt. Если вы нажмете Q, программа завершит свою работу. Листинг 3.7. Демонстрационная программа работы с клавиатурой (KEY.C). ------------------------------------------------------------------------- // ВКЛЮЧАЕМЫЕ ФАЙЛЫ ////////////////////////////////// // ОПРЕДЕЛЕНИЯ ////////////////////////////////////// // Значения скан-кодов. Внимание: каждая клавиша продуцирует только // один скан-код, поэтому определения даны только для символов // нижнего регистра. Например, одна и та же клавиша соответствует // символу "1" (нижний регистр) и символу "!" (верхний регистр). // Однако пользоваться надо все равно определением SCAN_1 // ФУНКЦИИ ////////////////////////////////////// unsigned char Get_Ascii_Key(void) { // Если в буфере клавиатуры есть символ, функция возвращает его // ASCII-код. Если символа нет, возвращается 0. if (_bios_keybrd(_KEYBRD_READY)) return(_bios_keybrd(_KEYBRD_READ)); else return(0); } // конец функции ///////////////////////////////////////////////////////////////////// unsigned int Get_Control_Keys(unsigned int mask) { // функция возвращает состояние любой управляющей клавиши return(mask & _bios_keybrd(_KEYBRD_SHIFTSTATUS)); } // конец функции //////////////////////////////////////////////////////////////////// - 72 - unsigned char Get_Scan_Code(void) { // функция возвращает скан-код нажатой клавиши // используется встроенный ассемблер _asm { mov ah,01h ; функция 01h - проверканажатия клавиши int 16h ; вызов прерывания jz empty ; нет символа - выход mov ah,00h ; функция 0 - получение скан-кода int 16h ; вызов прерывания mov al,ah ; перемещаем результат из AH в AL xor ah,ah ; обнуляем AH jmp done ; результат возвращается в AX empty: xor ax,ax ; обнуляем AX done: } // конец ассемблерного блока } // конец функции // ОСНОВНАЯ ПРОГРАММА /////////////////////////////////////////// void main(void) { unsigned char key; int done=0; unsigned int control; _clearscreen(_GCLEARSCREEN); while(!done) { _settextposition(2,0); if ( (key = Get_Scan_Code()) ) printf("%c %d ",key,key); // проверка на нажатие Ctrl и Alt if (Get_Control_Keys(CTRL)) printf("\ncontrol key pressed"); if (Get_Control_Keys(ALT)) printf("\nalt key pressed "); - 73 - if (key==16) done=1; // 16 - это скан-код клавиши Q } // конец цикла while } // конец функции main _______________________________________________________________________ М Ы Ш Ь Изобретение мыши, безусловно, было событием в компьютерной истории. Появившись в результате исследований в центре Xerox PARK в Калифорнии, она завоевала сердца миллионов пользователей. Мышь позволила наглядно работать с экраном и легко общаться с интересующими объектами. Обычно мышь подсоединяется к последовательному порту компьютера или DIN коннектору на лаптопе. Существующая bus mice требует специальной платы, поэтому не получила широкого распространения (это понятно - мало кто хочет просто так вскрывать свой компьютер для установки одной платы). На сегодняшний день существуют как механические, так и оптические мыши. В обоих случаях перемещение мыши кодируется в последовательность импульсов, которая преобразуется в пакет, содержащий информацию о движении мыши и состоянии кнопок. Этот пакет посылается в серийный порт, к которому подсоединяется мышь, а затем интерпретируется программой. Вообще-то, написание драйвера мыши - не самое скучное занятие, но мы этого делать не будем. К счастью, Microsoft и другие фирмы написали кучу подобных драйверов. Поэтому мы не станем самостоятельно декодировать мышиные пакеты, а воспользуемся более удобным программным интерфейсом. Мы будем использовать минимум функций для определения позиции мыши и статуса кнопок. В табл.3.3 перечислены эти функции. Замечание -------------- Микки (mickey) - это самое маленькое расстояние, которое отслеживается мышью. Оно примерно равно 1/200 дюйма. ------------- Таблица 3.3. Функции драйвера мыши. -------------------------------------------------------------------------- Bios INT 33h -------------------------------------------------------------------------- Функция 00h - инициализировать драйвер мыши -------------------------------------------------------------------------- Вход: AX: 0000h Выход: AX:FFFFh в случае успеха, 0000h при неудаче BX - количество кнопок мыши - 74 - ------------------------------------------------------------------------ Функция 01h - включить курсор мыши ------------------------------------------------------------------------- Вход: AX:0001h Выход: Ничего ------------------------------------------------------------------------ Функция 02h - выключить курсор мыши ------------------------------------------------------------------------ Вход: AX:0002h Выход: Ничего ------------------------------------------------------------------------- Функция 03h - возвратить позицию курсора и статус клавиш -------------------------------------------------------------------------- Вход: AX: 0003h Выход: BX: - статус кнопок Бит 0 - левая кнопка: 1 - нажата, 0 - не нажата Бит 1 - правая кнопка: 1 - нажата, 0 - не нажата Бит 2 - центральная кнопка: 1 - нажата, 0 - не нажата CX - X-координата курсора DX - Y-координата курсора ------------------------------------------------------------------------- Функция 0Bh - возвратить относительную позицию мыши ------------------------------------------------------------------------- Вход: AX:000Bh Выход: CX - относительное горизонтальное движение в mickey DX - относительное вертикальное движение в mickey ------------------------------------------------------------------------- Функция 1Ah - установить чувствительность ------------------------------------------------------------------------- Вход: AX:001Ah Выход: BX - чувствительность по оси X (0-100) CX - чувствительность по оси Y (0-100) DX - значение скорости, при которой чувствительность возрастает в 2 раза (0-100) ------------------------------------------------------------------------- Как видите, функции драйвера вызываются через прерывание 33h. Мы записываем параметр в регистр АХ и получаем результат в регистрах AX, BX, CX и DX. Я написал простую функцию для работы с мышью, она называется Squeeze_Mouse(). Эта функция может выполнять много действий - все зависит от передаваемых параметров. Прототип функции: - 75 - int Squeeze_Mouse(int command, int *x, int *y, int * buttons); Кроме этого, я сделал несколько описаний, чтобы упростить работу с ней: #define MOUSE_INT 0x33 // номер прерывания #define MOUSE_RESET 0x00 // сброс мыши #define MOUSE_SHOW 0x01 // показать мышь #define MOUSE_HIDE 0x02 // погасить мышь #define MOUSE_BUTT_POS 0x03 // возвратить координаты // и количество кнопок #define MOUSE_SET_SENSITIVITY 0x1A // установить // чувствительность в пределах 0-100 #define MOUSE_MOTION_REL 0x0B // установить // относительную чувствительность Таким образом, если мы хотим получить координаты мыши, то должны написать следующее: Squeeze_Mouse(MOUSE_BUTT_POS, &mouse_x, &mouse_y, &mouse_buttons); где: mouse_x,mouse_y и mouse_buttons - локальные переменные для сохранения результатов. Теперь обратим внимание на два способа, используемые для передачи координат мыши: - Драйвер мыши может возвращать абсолютные координаты. В этом случае значения X и Y являются координатами мыши на экране. К примеру, если мышь находится в левом верхнем углу экрана, функция возвращается (0,0); - Драйвер мыши может возвращать относительные координаты. При этом возвращается разница координат от предыдущей посылки. Например, если мышь подвинулась на 20 микки по оси Х и на 10 по оси Y, то эти значения и будут возвращены. Для чтения в относительном режиме используйте константу MOUSE_MOTION_REL. Еще несколько слов о мыши. Вы можете менятьее чувствительность к передвижению, используя константу MOUSE_SET_SENSITIVITY. Для этого подберите для переменных X и Y значение от 1 до 100 и вызовите Squeeze_Mouse. Чувствительность мыши определяется как отношение пиксельного перемещения курсора мыши к одному микки. Листинг 3.8 содержит демонстрационную программу, которая показывает использование мыши. Эта программа позволяет рисовать на экране, нажимая на левую кнопку мыши и менять цвет, используя правую кнопку. - 76 - Листинг 3.8. Работа с мышью (MOUSE.C). ------------------------------------------------------------------------- // ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////////////////// #include #include #include #include #include #include // ОПРЕДЕЛЕНИЯ ///////////////////////////////////////////////// // вызовы функций мыши #define MOUSE_INT 0x33 // Номер прерывания мыши #define MOUSE_RESET 0х00 // Инициализация драйвера #define MOUSE_SHOW 0х01 // Показать курсор мыши #define MOUSE_HIDE 0х02 // Спрятать курсор мыши #define MOUSE_BUTT_POS 0х03 // Получение полного статуса #define MOUSE_SET_SENSITIVITY 0х1A // Установка чувствительности #define MOUSE_MOTION_REL 0х0B // Получить значение счетчика микки #define MOUSE_LEFT_BUTTON 0х01 // левая кнопка #define MOUSE_RIGHT_BUTTON 0х02 // правая кнопка #define MOUSE_CENTER_BUTTON 0х04 // средняя кнопка // ФУНКЦИИ /////////////////////////////////////////////////////// int Squeeze_Mouse(int command, int *x, int *y,int *buttons) { // Мы будем использовать _int86 вместо встроенного ассемблера // Почему ? И сам не знаю union _REGS inregs, outregs; switch(command) { case MOUSE_RESET: { inregs.x.ax = 0x00; // подфункция 0 - инициализация _int86(MOUSE_INT, &inregs, &outregs); *buttons = outregs.x.bx; // возвратить количество кнопок return(outregs.x.ax); //возвратить общий результат } break; - 77 - case MOUSE_SHOW: { // эта функция инкрементирует счетчик драйвера. Когда значение // счетчика становится больше или равно 0, курсор появляется на экране inregs.x.ax = 0x01; // подфункция 1 - показать курсор _int86(MOUSE_INT, &inregs, &outregs); return(1); } break; case MOUSE_HIDE: { // эта функция декрементирует счетчик драйвера, когда его // значение становится меньше 0, курсор исчезает с экрана inregs.x.ax 0x02; // подфункция 2 - спрятать курсор _int86(MOUSE_INT, &inregs, &outregs); return(1); } break; case MOUSE_BUTT_POS: { // эта функция позволяет получить полный статус состояния мыши, // включая абсолютную позицию курсора в координатах (х,у) и // состояние кнопок inregs.x.ax = 0x03; // подфункция 3 - получить статус мыши _int86(MOUSE_INT, &inregs, &outregs); // извлечь информацию и вернуть ее через указатели *x = outregs.x.cx; *y = outregs.x.dx; *buttons = outregs.x.bx; return(1); } break; case MOUSE_MOTION_REL: { // эта функция позволяет получить относительное изменение // координат мыши с момента последнего вызова inregs.x.ax = 0x03 // подфункция 1 - получить // относительную позицию - 78 - _int86(MOUSE_INT, &inregs, &outregs); // результат при помощи указателей помещается в переменные х и у *x = outregs.x.cx; *y = outregs.x.dx; return(1); } break; case MOUSE_SET_SENSITIVITY: { // эта функция устанавливает чувствительность мыши. Перед // вызовом необходимо установить переменные х и у в значения // из диапазона 1-100. Переменная "buttons" используется для // установки значения порога удвоения скорости // (из диапазона 1-100) inregs.x.bx = *x; inregs.x.cx = *y; inregs.x.dx = *buttons; inregs.x.ax = 0x1A; // подфункция 26 - установка // чувствительности _int86(MOUSE_INT, &inregs, &outregs); return(1); } break; default:break; } // конец оператора switch } // конец функции // ОСНОВНАЯ ПРОГРАММА /////////////////////////////////////////////// void main(void) { int x,y,buttons,num_buttons; int color=1; _setvideomode(_VRES16COLOR); // 640х480, 16 цветов // инициализация драйвера мыши Squeeze_Mouse(MOUSE_RESET,NULL,NULL,&num_buttons); - 79 - // показать курсор Squeeze_Mouse(MOUSE_SHOW,NULL,NULL,NULL); while(!kbhit()) { _settextposition(2,0); Squeeze_Mouse(MOUSE_BUTT_POS,&x,&y,&buttons); printf("mouse x=%d y=%d buttons=%d ",x,y,buttons); //рисование if (buttons==1) { _setcolor(color); _setpixel(x-1,y-2); _setpixel(x,y-2); _setpixel(x-1,y-1); -setpixel(x,y-1); } // конец обработки нажатия левой кнопки // выбор цвета if (buttons==2) { if (++color>15 color=0; // ждем отпускания правой кнопки while(buttons==2) { Squeeze_Mouse(MOUSE_BUTT_POS,&x,&y,&buttons); } // конец ожидания } // конец графической работы } // конец цикла while // назад в текстовый режим _setvideomode(_DEFAULTMODE); } // конец функции main ------------------------------------------------------------------------- - 80 - ИТОГ В этой главе мы изучили разные устройства ввода, которые могут использоваться в видеоиграх: клавиатура, джойстик, мышь. Более того, мы даже написали функции для работы с этими устройствами. Еще мы выяснили, что не стоит использовать BIOS для работы с устройствами ввода, за исключением клавиатуры.